#include "common/pch.h"
#include "map/terrain.h"

#include <algorithm>

#define VMAP_INVALID_HEIGHT       -100000.0f            // for check
#define VMAP_INVALID_HEIGHT_VALUE -200000.0f            // real assigned value in unknown height case

static unsigned int urand(unsigned int minVal, unsigned int maxVal)
{
    if (minVal == maxVal) {
        return minVal;
    }

    if (minVal > maxVal) {
        int tmp = minVal;
        minVal = maxVal;
        maxVal = tmp;
    }

    return minVal + std::max((unsigned int)0, (unsigned int)(rand() % (maxVal - minVal + 1)));
}

TerrainInfo::TerrainInfo(unsigned int mapid) : m_mapId(mapid)
#if 0  
m_refMutex(), m_mutex()
#endif
{
    for (int k = 0; k < MAX_NUMBER_OF_GRIDS; ++k)
    {
        for (int i = 0; i < MAX_NUMBER_OF_GRIDS; ++i)
        {
            m_GridMaps[i][k] = NULL;
            m_GridRef[i][k] = 0;
        }
    }

    // clean up GridMap objects every minute
    const unsigned int iCleanUpInterval = 60;
    // schedule start randlomly
    const unsigned int iRandomStart = urand(20, 40);

#if 0
    i_timer.SetInterval(iCleanUpInterval * 1000);
    i_timer.SetCurrent(iRandomStart * 1000);
#endif
}

TerrainInfo::~TerrainInfo()
{
    for (int k = 0; k < MAX_NUMBER_OF_GRIDS; ++k)
        for (int i = 0; i < MAX_NUMBER_OF_GRIDS; ++i)
        {
            delete m_GridMaps[i][k];
        }

#if 0
    VMAP::VMapFactory::createOrGetVMapManager()->unloadMap(m_mapId);
    MMAP::MMapFactory::createOrGetMMapManager()->unloadMap(m_mapId);
#endif
}

GridMap* TerrainInfo::Load(const unsigned int x, const unsigned int y)
{
#if 0
    CFT_ASSERT(x < MAX_NUMBER_OF_GRIDS);
    CFT_ASSERT(y < MAX_NUMBER_OF_GRIDS);
#endif

    // reference grid as a first step
    RefGrid(x, y);

    // quick check if GridMap already loaded
    GridMap* pMap = m_GridMaps[x][y];
    if (!pMap)
    {
        pMap = LoadMapAndVMap(x, y);
    }

    return pMap;
}

// schedule lazy GridMap object cleanup
void TerrainInfo::Unload(const unsigned int x, const unsigned int y)
{
#if 0
    CFT_ASSERT(x < MAX_NUMBER_OF_GRIDS);
    CFT_ASSERT(y < MAX_NUMBER_OF_GRIDS);
#endif

    if (m_GridMaps[x][y])
    {
        // decrease grid reference count...
        if (UnrefGrid(x, y) == 0)
        {
            // TODO: add your additional logic here
        }
    }
}

// call this method only
void TerrainInfo::CleanUpGrids(const unsigned int diff)
{
#if 0
    i_timer.Update(diff);
    if (!i_timer.Passed())
    {
        return;
    }

    for (int y = 0; y < MAX_NUMBER_OF_GRIDS; ++y)
    {
        for (int x = 0; x < MAX_NUMBER_OF_GRIDS; ++x)
        {
            const short& iRef = m_GridRef[x][y];
            GridMap* pMap = m_GridMaps[x][y];

            // delete those GridMap objects which have refcount = 0
            if (pMap && iRef == 0)
            {
                m_GridMaps[x][y] = NULL;
                // delete grid data if reference count == 0
                pMap->unloadData();
                delete pMap;

                // unload VMAPS...
                VMAP::VMapFactory::createOrGetVMapManager()->unloadMap(m_mapId, x, y);

                // unload mmap...
                MMAP::MMapFactory::createOrGetMMapManager()->unloadMap(m_mapId, x, y);
            }
        }
    }

    i_timer.Reset();
#endif
}

int TerrainInfo::RefGrid(const unsigned int& x, const unsigned int& y)
{
    CFT_ASSERT(x < MAX_NUMBER_OF_GRIDS);
    CFT_ASSERT(y < MAX_NUMBER_OF_GRIDS);

#if 0
    ACE_GUARD_RETURN(LOCK_TYPE, _lock, m_refMutex, -1)
#endif
        return (m_GridRef[x][y] += 1);
}

int TerrainInfo::UnrefGrid(const unsigned int& x, const unsigned int& y)
{
    CFT_ASSERT(x < MAX_NUMBER_OF_GRIDS);
    CFT_ASSERT(y < MAX_NUMBER_OF_GRIDS);

    short& iRef = m_GridRef[x][y];

#if 0
    ACE_GUARD_RETURN(LOCK_TYPE, _lock, m_refMutex, -1)
#endif
        if (iRef > 0)
        {
            return (iRef -= 1);
        }

    return 0;
}

float TerrainInfo::GetHeightStatic(float x, float y, float z, bool useVmaps/*=true*/, float maxSearchDist/*=DEFAULT_HEIGHT_SEARCH*/) const
{
    float mapHeight = VMAP_INVALID_HEIGHT_VALUE;            // Store Height obtained by maps
    float vmapHeight = VMAP_INVALID_HEIGHT_VALUE;           // Store Height obtained by vmaps (in "corridor" of z (or slightly above z)

    float z2 = z + 2.f;

    // find raw .map surface under Z coordinates (or well-defined above)
    if (GridMap* gmap = const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        mapHeight = gmap->getHeight(x, y);
    }

#if 0
    if (useVmaps)
    {
        VMAP::IVMapManager* vmgr = VMAP::VMapFactory::createOrGetVMapManager();
        if (vmgr->isHeightCalcEnabled())
        {
            // if mapHeight has been found search vmap height at least until mapHeight point
            // this prevent case when original Z "too high above ground and vmap height search fail"
            // this will not affect most normal cases (no map in instance, or stay at ground at continent)
            if (mapHeight > INVALID_HEIGHT && z2 - mapHeight > maxSearchDist)
            {
                maxSearchDist = z2 - mapHeight + 1.0f;       // 1.0 make sure that we not fail for case when map height near but above for vamp height
            }

            // look from a bit higher pos to find the floor
            vmapHeight = vmgr->getHeight(GetMapId(), x, y, z2, maxSearchDist);

            // if not found in expected range, look for infinity range (case of far above floor, but below terrain-height)
            if (vmapHeight <= INVALID_HEIGHT)
            {
                vmapHeight = vmgr->getHeight(GetMapId(), x, y, z2, 10000.0f);
            }

            // still not found, look near terrain height
            if (vmapHeight <= INVALID_HEIGHT && mapHeight > INVALID_HEIGHT && z2 < mapHeight)
            {
                vmapHeight = vmgr->getHeight(GetMapId(), x, y, mapHeight + 2.0f, DEFAULT_HEIGHT_SEARCH);
            }
        }
    }
#endif

    // mapHeight set for any above raw ground Z or <= INVALID_HEIGHT
    // vmapheight set for any under Z value or <= INVALID_HEIGHT
    if (vmapHeight > INVALID_HEIGHT)
    {
        if (mapHeight > INVALID_HEIGHT)
        {
            // we have mapheight and vmapheight and must select more appropriate

            // we are already under the surface or vmap height above map heigt
            if (z < mapHeight || vmapHeight > mapHeight)
            {
                return vmapHeight;
            }
            else
            {
                return mapHeight;                            // better use .map surface height
            }
        }
        else
        {
            return vmapHeight;                               // we have only vmapHeight (if have)
        }
    }

    return mapHeight;
}

inline bool IsOutdoorWMO(unsigned int mogpFlags)
{
    return mogpFlags & 0x8000;
}

bool TerrainInfo::IsOutdoors(float x, float y, float z) const
{
    unsigned int mogpFlags;
    int adtId, rootId, groupId;

    // no wmo found? -> outside by default
    if (!GetAreaInfo(x, y, z, mogpFlags, adtId, rootId, groupId))
    {
        return true;
    }

    return IsOutdoorWMO(mogpFlags);
}

bool TerrainInfo::GetAreaInfo(float x, float y, float z, unsigned int& flags, int& adtId, int& rootId, int& groupId) const
{
    float vmap_z = z;
#if 0
    VMAP::IVMapManager* vmgr = VMAP::VMapFactory::createOrGetVMapManager();
    if (vmgr->getAreaInfo(GetMapId(), x, y, vmap_z, flags, adtId, rootId, groupId))
    {
        // check if there's terrain between player height and object height
        if (GridMap* gmap = const_cast<TerrainInfo*>(this)->GetGrid(x, y))
        {
            float _mapheight = gmap->getHeight(x, y);
            // z + 2.0f condition taken from GetHeightStatic(), not sure if it's such a great choice...
            if (z + 2.0f > _mapheight && _mapheight > vmap_z)
            {
                return false;
            }
        }
        return true;
    }
#endif
    return false;
}

unsigned short TerrainInfo::GetAreaFlag(float x, float y, float z, bool* isOutdoors) const
{
#if 1
    return 0;
#else
    unsigned int mogpFlags;
    int adtId, rootId, groupId;
    WMOAreaTableEntry const* wmoEntry = 0;
    AreaTableEntry const* atEntry = 0;
    bool haveAreaInfo = false;

    if (GetAreaInfo(x, y, z, mogpFlags, adtId, rootId, groupId))
    {
        haveAreaInfo = true;
        wmoEntry = GetWMOAreaTableEntryByTripple(rootId, adtId, groupId);
        if (wmoEntry)
        {
            atEntry = GetAreaEntryByAreaID(wmoEntry->areaId);
        }
    }

    unsigned short areaflag;
    if (atEntry)
    {
        areaflag = atEntry->exploreFlag;
    }
    else
    {
        if (GridMap* gmap = const_cast<TerrainInfo*>(this)->GetGrid(x, y))
        {
            areaflag = gmap->getArea(x, y);
        }
        // this used while not all *.map files generated (instances)
        else
        {
            areaflag = GetAreaFlagByMapId(GetMapId());
        }
    }

    if (isOutdoors)
    {
        if (haveAreaInfo)
        {
            *isOutdoors = IsOutdoorWMO(mogpFlags);
        }
        else
        {
            *isOutdoors = true;
        }
    }
    return areaflag;
#endif
}

unsigned char TerrainInfo::GetTerrainType(float x, float y) const
{
    if (GridMap* gmap = const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        return gmap->getTerrainType(x, y);
    }
    else
    {
        return 0;
    }
}

unsigned int TerrainInfo::GetAreaId(float x, float y, float z) const
{
#if 1
    return 0;
#else
    return TerrainManager::GetAreaIdByAreaFlag(GetAreaFlag(x, y, z), m_mapId);
#endif
}

unsigned int TerrainInfo::GetZoneId(float x, float y, float z) const
{
#if 1
    return 0;
#else
    return TerrainManager::GetZoneIdByAreaFlag(GetAreaFlag(x, y, z), m_mapId);
#endif
}

void TerrainInfo::GetZoneAndAreaId(unsigned int& zoneid, unsigned int& areaid, float x, float y, float z) const
{
#if 0
    TerrainManager::GetZoneAndAreaIdByAreaFlag(zoneid, areaid, GetAreaFlag(x, y, z), m_mapId);
#endif
}

GridMapLiquidStatus TerrainInfo::getLiquidStatus(float x, float y, float z, unsigned char ReqLiquidType, GridMapLiquidData* data) const
{
    GridMapLiquidStatus result = LIQUID_MAP_NO_WATER;
#if 0
    VMAP::IVMapManager* vmgr = VMAP::VMapFactory::createOrGetVMapManager();
    unsigned int liquid_type = 0;
    float liquid_level = INVALID_HEIGHT_VALUE;
    float ground_level = GetHeightStatic(x, y, z, true, DEFAULT_WATER_SEARCH);

    if (vmgr->GetLiquidLevel(GetMapId(), x, y, z, ReqLiquidType, liquid_level, ground_level, liquid_type))
    {
        //DEBUG_LOG("getLiquidStatus(): vmap liquid level: %f ground: %f type: %u", liquid_level, ground_level, liquid_type);
        // Check water level and ground level
        if (liquid_level > ground_level && z > ground_level - 2)
        {
            // All ok in water -> store data
            if (data)
            {
                unsigned int liquidFlagType = 0;
                if (LiquidTypeEntry const* liq = sLiquidTypeStore.LookupEntry(liquid_type))
                {
                    liquidFlagType = 1 << liq->Type;
                }

                data->level = liquid_level;
                data->depth_level = ground_level;

                data->entry = liquid_type;
                data->type_flags = liquidFlagType;
            }

            // For speed check as int values
            int delta = int((liquid_level - z) * 10);

            // Get position delta
            if (delta > 20)                   // Under water
            {
                return LIQUID_MAP_UNDER_WATER;
            }
            if (delta > 0)                    // In water
            {
                return LIQUID_MAP_IN_WATER;
            }
            if (delta > -1)                   // Walk on water
            {
                return LIQUID_MAP_WATER_WALK;
            }
            result = LIQUID_MAP_ABOVE_WATER;
        }
    }
    else if (GridMap* gmap = const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        GridMapLiquidData map_data;
        GridMapLiquidStatus map_result = gmap->getLiquidStatus(x, y, z, ReqLiquidType, &map_data);
        // Not override LIQUID_MAP_ABOVE_WATER with LIQUID_MAP_NO_WATER:
        if (map_result != LIQUID_MAP_NO_WATER && (map_data.level > ground_level))
        {
            if (data)
            {
                *data = map_data;
            }

            return map_result;
        }
    }
#endif
    return result;
}

bool TerrainInfo::IsInWater(float x, float y, float pZ, GridMapLiquidData* data) const
{
    // Check surface in x, y point for liquid
    if (const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        GridMapLiquidData liquid_status;
        GridMapLiquidData* liquid_ptr = data ? data : &liquid_status;
        if (getLiquidStatus(x, y, pZ, MAP_ALL_LIQUIDS, liquid_ptr))
        {
            // if (liquid_prt->level - liquid_prt->depth_level > 2) //???
            return true;
        }
    }
    return false;
}

// check if creature is in water and have enough space to swim
bool TerrainInfo::IsSwimmable(float x, float y, float pZ, float radius /*= 1.5f*/, GridMapLiquidData* data /*= 0*/) const
{
    // Check surface in x, y point for liquid
    if (const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        GridMapLiquidData liquid_status;
        GridMapLiquidData* liquid_ptr = data ? data : &liquid_status;
        if (getLiquidStatus(x, y, pZ, MAP_ALL_LIQUIDS, liquid_ptr))
        {
            if (liquid_ptr->level - liquid_ptr->depth_level > radius) // is unit have enough space to swim
            {
                return true;
            }
        }
    }
    return false;
}

bool TerrainInfo::IsUnderWater(float x, float y, float z) const
{
    if (const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        if (getLiquidStatus(x, y, z, MAP_LIQUID_TYPE_WATER | MAP_LIQUID_TYPE_OCEAN) & LIQUID_MAP_UNDER_WATER)
        {
            return true;
        }
    }
    return false;
}

/**
 * Function find higher form water or ground height for current floor
 *
 * @param x, y, z    Coordinates original point at floor level
 *
 * @param pGround    optional arg for retrun calculated by function work ground height, it let avoid in caller code recalculate height for point if it need
 *
 * @param swim       z coordinate can be calculated for select above/at or under z coordinate (for fly or swim/walking by bottom)
 *                   in last cases for in water returned under water height for avoid client set swimming unit as saty at water.
 *
 * @return           calculated z coordinate
 */
float TerrainInfo::GetWaterOrGroundLevel(float x, float y, float z, float* pGround /*= NULL*/, bool swim /*= false*/) const
{
    if (const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        // we need ground level (including grid height version) for proper return water level in point
        float ground_z = GetHeightStatic(x, y, z, true, DEFAULT_WATER_SEARCH);
        if (pGround)
        {
            *pGround = ground_z;
        }

        GridMapLiquidData liquid_status;

        GridMapLiquidStatus res = getLiquidStatus(x, y, ground_z, MAP_ALL_LIQUIDS, &liquid_status);
        return res ? (swim ? liquid_status.level - 2.0f : liquid_status.level) : ground_z;
    }

    return VMAP_INVALID_HEIGHT_VALUE;
}

GridMap* TerrainInfo::GetGrid(const float x, const float y)
{
    // half opt method
    int gx = (int)(32 - x / SIZE_OF_GRIDS);                 // grid x
    int gy = (int)(32 - y / SIZE_OF_GRIDS);                 // grid y

    // quick check if GridMap already loaded
    GridMap* pMap = m_GridMaps[gx][gy];
    if (!pMap)
    {
        pMap = LoadMapAndVMap(gx, gy);
    }

    return pMap;
}

GridMap* TerrainInfo::LoadMapAndVMap(const unsigned int x, const unsigned int y)
{
    // double checked lock pattern
    if (!m_GridMaps[x][y])
    {
#if 0
        ACE_GUARD_RETURN(LOCK_TYPE, lock, m_mutex, NULL)

            if (!m_GridMaps[x][y])
            {
                GridMap* map = new GridMap();

                // map file name
                int len = sWorld.GetDataPath().length() + strlen("maps/%03u%02u%02u.map") + 1;
                char* tmp = new char[len];
                snprintf(tmp, len, (char*)(sWorld.GetDataPath() + "maps/%03u%02u%02u.map").c_str(), m_mapId, x, y);
                DEBUG_FILTER_LOG(LOG_FILTER_MAP_LOADING, "Loading map %s", tmp);

                if (!map->loadData(tmp))
                {
                    sLog.outError("Error load map file: \n %s\n", tmp);
                    // ASSERT(false);
                }

                delete[] tmp;
                m_GridMaps[x][y] = map;

                // load VMAPs for current map/grid...
                const MapEntry* i_mapEntry = sMapStore.LookupEntry(m_mapId);
                const char* mapName = i_mapEntry ? i_mapEntry->name[sWorld.GetDefaultDbcLocale()] : "UNNAMEDMAP\x0";

                int vmapLoadResult = VMAP::VMapFactory::createOrGetVMapManager()->loadMap((sWorld.GetDataPath() + "vmaps").c_str(), m_mapId, x, y);
                switch (vmapLoadResult)
                {
                case VMAP::VMAP_LOAD_RESULT_OK:
                    DEBUG_FILTER_LOG(LOG_FILTER_MAP_LOADING, "VMAP loaded name:%s, id:%d, x:%d, y:%d (vmap rep.: x:%d, y:%d)", mapName, m_mapId, x, y, x, y);
                    break;
                case VMAP::VMAP_LOAD_RESULT_ERROR:
                    DEBUG_FILTER_LOG(LOG_FILTER_MAP_LOADING, "Could not load VMAP name:%s, id:%d, x:%d, y:%d (vmap rep.: x:%d, y:%d)", mapName, m_mapId, x, y, x, y);
                    break;
                case VMAP::VMAP_LOAD_RESULT_IGNORED:
                    DEBUG_FILTER_LOG(LOG_FILTER_MAP_LOADING, "Ignored VMAP name:%s, id:%d, x:%d, y:%d (vmap rep.: x:%d, y:%d)", mapName, m_mapId, x, y, x, y);
                    break;
                }

                // load navmesh
                MMAP::MMapFactory::createOrGetMMapManager()->loadMap(m_mapId, x, y);
            }
#endif
    }

    return  m_GridMaps[x][y];
}

float TerrainInfo::GetWaterLevel(float x, float y, float z, float* pGround /*= NULL*/) const
{
    if (const_cast<TerrainInfo*>(this)->GetGrid(x, y))
    {
        // we need ground level (including grid height version) for proper return water level in point
        float ground_z = GetHeightStatic(x, y, z, true, DEFAULT_WATER_SEARCH);
        if (pGround)
        {
            *pGround = ground_z;
        }

        GridMapLiquidData liquid_status;

        GridMapLiquidStatus res = getLiquidStatus(x, y, ground_z, MAP_ALL_LIQUIDS, &liquid_status);
        if (!res)
        {
            return VMAP_INVALID_HEIGHT_VALUE;
        }

        return liquid_status.level;
    }

    return VMAP_INVALID_HEIGHT_VALUE;
}
